Spring

您所在的位置:网站首页 播放spring spring Spring

Spring

#Spring| 来源: 网络整理| 查看: 265

服务端如何将一个大视频文件做切分,分段响应给客户端,让浏览器可以渐进式地播放。

Spring-Boot实现HTTP分片下载断点续传,从而实现H5页面的大视频播放问题,实现渐进式播放,每次只播放需要播放的内容就可以了,不需要加载整个文件到内存中。

文件的断点续传、文件多线程并发下载(迅雷就是这么玩的)等。

代码实现 package com.example.insurance.controller; import com.example.insurance.common.MediaContentUtil; import cn.hutool.core.io.IORuntimeException; import cn.hutool.core.io.IoUtil; import cn.hutool.core.io.NioUtil; import cn.hutool.core.io.StreamProgress; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.catalina.connector.ClientAbortException; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpRange; import org.springframework.http.HttpStatus; import org.springframework.util.CollectionUtils; import org.springframework.util.StopWatch; import org.springframework.util.unit.DataSize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.BufferedOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.RandomAccessFile; import java.nio.MappedByteBuffer; import java.nio.channels.Channels; import java.nio.channels.FileChannel; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.util.List; /** * 内容资源控制器 */ @SuppressWarnings("unused") @Slf4j @RestController("resourceController") @RequestMapping(path = "/resource", produces = MediaType.APPLICATION_JSON_VALUE) public class ResourceController { /** * 获取文件内容 * * @param fileName 内容文件名称 * @param response 响应对象 */ @GetMapping("/media/{fileName}") public void getMedia(@PathVariable String fileName, HttpServletRequest request, HttpServletResponse response, @RequestHeader HttpHeaders headers) { // printRequestInfo(fileName, request, headers); String filePath = MediaContentUtil.filePath(); try { this.download(fileName, filePath, request, response, headers); } catch (Exception e) { log.error("getMedia error, fileName={}", fileName, e); } } /** * 获取封面内容 * * @param fileName 内容封面名称 * @param response 响应对象 */ @GetMapping("/cover/{fileName}") public void getCover(@PathVariable String fileName, HttpServletRequest request, HttpServletResponse response, @RequestHeader HttpHeaders headers) { // printRequestInfo(fileName, request, headers); String filePath = MediaContentUtil.filePath(); try { this.download(fileName, filePath, request, response, headers); } catch (Exception e) { log.error("getCover error, fileName={}", fileName, e); } } // ======= internal ======= private static void printRequestInfo(String fileName, HttpServletRequest request, HttpHeaders headers) { String requestUri = request.getRequestURI(); String queryString = request.getQueryString(); log.debug("file={}, url={}?{}", fileName, requestUri, queryString); log.info("headers={}", headers); } /** * 缓冲区大小 16KB * * @see NioUtil#DEFAULT_BUFFER_SIZE * @see NioUtil#DEFAULT_LARGE_BUFFER_SIZE */ // private static final int BUFFER_SIZE = NioUtil.DEFAULT_MIDDLE_BUFFER_SIZE; private static final int BUFFER_SIZE = (int) DataSize.ofKilobytes(16L).toBytes(); private static final String BYTES_STRING = "bytes"; /** * 设置请求响应状态、头信息、内容类型与长度 等。 * * * HTTP/1.1 Range Requests * 2. Range Units * 4. Responses to a Range Request * * * HTTP/1.1 * 10.2.7 206 Partial Content * 14.5 Accept-Ranges * 14.13 Content-Length * 14.16 Content-Range * 14.17 Content-Type * 19.5.1 Content-Disposition * 15.5 Content-Disposition Issues * * * Content-Disposition * 2. The Content-Disposition Header Field * 2.1 The Inline Disposition Type * 2.3 The Filename Parameter * * * @param response 请求响应对象 * @param fileName 请求的文件名称 * @param contentType 内容类型 * @param contentRange 内容范围对象 */ private static void setResponse( HttpServletResponse response, String fileName, String contentType, ContentRange contentRange) { // http状态码要为206:表示获取部分内容 response.setStatus(HttpStatus.PARTIAL_CONTENT.value()); // 支持断点续传,获取部分字节内容 // Accept-Ranges:bytes,表示支持Range请求 response.setHeader(HttpHeaders.ACCEPT_RANGES, BYTES_STRING); // inline表示浏览器直接使用,attachment表示下载,fileName表示下载的文件名 response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "inline;filename=" + MediaContentUtil.encode(fileName)); // Content-Range,格式为:[要下载的开始位置]-[结束位置]/[文件总大小] // Content-Range: bytes 0-10/3103,格式为bytes 开始-结束/全部 response.setHeader(HttpHeaders.CONTENT_RANGE, toContentRange(contentRange)); response.setContentType(contentType); // Content-Length: 11,本次内容的大小 response.setContentLengthLong(applyAsContentLength(contentRange)); } /** * 组装内容范围的响应头。 * * * 4.2. Content-Range - HTTP/1.1 Range Requests * Content-Range: "bytes" first-byte-pos "-" last-byte-pos "/" complete-length * * For example: * Content-Range: bytes 0-499/1234 * * * @param range 内容范围对象 * @return 内容范围的响应头 */ private static String toContentRange(ContentRange range) { return BYTES_STRING + ' ' + range.start + '-' + range.end + '/' + range.length; } /** * 计算内容完整的长度/总长度。 * * @param range 内容范围对象 * @return 内容完整的长度/总长度 */ private static long applyAsContentLength(ContentRange range) { return range.end - range.start + 1; } /** * * Spring Boot 处理 HTTP Headers */ private void download( String fileName, String path, HttpServletRequest request, HttpServletResponse response, HttpHeaders headers) throws IOException { Path filePath = Paths.get(path + fileName); if (!Files.exists(filePath)) { log.warn("file not exist, filePath={}", filePath); return; } long fileLength = Files.size(filePath); // long fileLength2 = filePath.toFile().length() - 1; // // fileLength=1184856, fileLength2=1184855 // log.info("fileLength={}, fileLength2={}", fileLength, fileLength2); // 开始下载位置 long firstBytePos; // 结束下载位置 long lastBytePos; /* * 3.1. Range - HTTP/1.1 Range Requests * https://www.rfc-editor.org/rfc/rfc7233#section-3.1 * Range: "bytes" "=" first-byte-pos "-" [ last-byte-pos ] * * For example: * bytes=0- * bytes=0-499 */ // Range:告知服务端,客户端下载该文件想要从指定的位置开始下载 List httpRanges = headers.getRange(); if (CollectionUtils.isEmpty(httpRanges)) { firstBytePos = 0; lastBytePos = fileLength - 1; } else { HttpRange httpRange = httpRanges.get(0); firstBytePos = httpRange.getRangeStart(fileLength); lastBytePos = httpRange.getRangeEnd(fileLength); } ContentRange contentRange = new ContentRange(firstBytePos, lastBytePos, fileLength); String range = request.getHeader(HttpHeaders.RANGE); // httpRanges=[], range=null // httpRanges=[448135688-], range=bytes=448135688- log.debug("httpRanges={}, range={}", httpRanges, range); // 要下载的长度 long contentLength = applyAsContentLength(contentRange); log.debug("contentRange={}, contentLength={}", contentRange, contentLength); // 文件类型 String contentType = request.getServletContext().getMimeType(fileName); // mimeType=video/mp4, CONTENT_TYPE=null log.debug("mimeType={}, CONTENT_TYPE={}", contentType, request.getContentType()); setResponse(response, fileName, contentType, contentRange); // 耗时指标统计 StopWatch stopWatch = new StopWatch("downloadFile"); stopWatch.start(fileName); try { // case-1.参考网上他人的实现 // if (fileLength >= Integer.MAX_VALUE) { // copy(filePath, response, contentRange); // } else { // copyByChannelAndBuffer(filePath, response, contentRange); // } // case-2.使用现成API copyByBio(filePath, response, contentRange); // copyByNio(filePath, response, contentRange); // case-3.视频分段渐进式播放 // if (contentType.startsWith("video")) { // copyForBufferSize(filePath, response, contentRange); // } else { // // 图片、PDF等文件 // copyByBio(filePath, response, contentRange); // } } finally { stopWatch.stop(); log.info("download file, fileName={}, time={} ms", fileName, stopWatch.getTotalTimeMillis()); } } /** * * * Java后端实现视频分段渐进式播放 * 服务端如何将一个大的视频文件做切分,分段响应给客户端,让浏览器可以渐进式地播放。 * 文件的断点续传、文件多线程并发下载(迅雷就是这么玩的)等。 * * * 大文件分片上传前后端实现 * */ private static void copyForBufferSize(Path filePath, HttpServletResponse response, ContentRange contentRange) { String fileName = filePath.getFileName().toString(); RandomAccessFile randomAccessFile = null; OutputStream outputStream = null; try { // 随机读文件 randomAccessFile = new RandomAccessFile(filePath.toFile(), "r"); // 移动访问指针到指定位置 randomAccessFile.seek(contentRange.start); // 注意:缓冲区大小 2MB,视频加载正常;1MB时有部分视频加载失败 int bufferSize = BUFFER_SIZE; //获取响应的输出流 outputStream = new BufferedOutputStream(response.getOutputStream(), bufferSize); // 每次请求只返回1MB的视频流 byte[] buffer = new byte[bufferSize]; int len = randomAccessFile.read(buffer); //设置此次相应返回的数据长度 response.setContentLength(len); // 将这1MB的视频流响应给客户端 outputStream.write(buffer, 0, len); log.info("file download complete, fileName={}, contentRange={}", fileName, toContentRange(contentRange)); } catch (ClientAbortException | IORuntimeException e) { // 捕获此异常表示用户停止下载 log.warn("client stop file download, fileName={}", fileName); } catch (Exception e) { log.error("file download error, fileName={}", fileName, e); } finally { IoUtil.close(outputStream); IoUtil.close(randomAccessFile); } } /** * 拷贝流,拷贝后关闭流。 * * @param filePath 源文件路径 * @param response 请求响应 * @param contentRange 内容范围 */ private static void copyByBio(Path filePath, HttpServletResponse response, ContentRange contentRange) { String fileName = filePath.getFileName().toString(); InputStream inputStream = null; OutputStream outputStream = null; try { RandomAccessFile randomAccessFile = new RandomAccessFile(filePath.toFile(), "r"); randomAccessFile.seek(contentRange.start); inputStream = Channels.newInputStream(randomAccessFile.getChannel()); outputStream = new BufferedOutputStream(response.getOutputStream(), BUFFER_SIZE); StreamProgress streamProgress = new StreamProgressImpl(fileName); long transmitted = IoUtil.copy(inputStream, outputStream, BUFFER_SIZE, streamProgress); log.info("file download complete, fileName={}, transmitted={}", fileName, transmitted); } catch (ClientAbortException | IORuntimeException e) { // 捕获此异常表示用户停止下载 log.warn("client stop file download, fileName={}", fileName); } catch (Exception e) { log.error("file download error, fileName={}", fileName, e); } finally { IoUtil.close(outputStream); IoUtil.close(inputStream); } } /** * 拷贝流,拷贝后关闭流。 * * * Java NIO 学习笔记(一)----概述,Channel/Buffer * * * @param filePath 源文件路径 * @param response 请求响应 * @param contentRange 内容范围 */ private static void copyByNio(Path filePath, HttpServletResponse response, ContentRange contentRange) { String fileName = filePath.getFileName().toString(); InputStream inputStream = null; OutputStream outputStream = null; try { RandomAccessFile randomAccessFile = new RandomAccessFile(filePath.toFile(), "r"); randomAccessFile.seek(contentRange.start); inputStream = Channels.newInputStream(randomAccessFile.getChannel()); outputStream = new BufferedOutputStream(response.getOutputStream(), BUFFER_SIZE); StreamProgress streamProgress = new StreamProgressImpl(fileName); long transmitted = NioUtil.copyByNIO(inputStream, outputStream, BUFFER_SIZE, streamProgress); log.info("file download complete, fileName={}, transmitted={}", fileName, transmitted); } catch (ClientAbortException | IORuntimeException e) { // 捕获此异常表示用户停止下载 log.warn("client stop file download, fileName={}", fileName); } catch (Exception e) { log.error("file download error, fileName={}", fileName, e); } finally { IoUtil.close(outputStream); IoUtil.close(inputStream); } } /** * * * SpringBoot Java实现Http方式分片下载断点续传+实现H5大视频渐进式播放 * SpringBoot 实现Http分片下载断点续传,从而实现H5页面的大视频播放问题,实现渐进式播放,每次只播放需要播放的内容就可以了,不需要加载整个文件到内存中。 * 二、Http分片下载断点续传实现 * 四、缓存文件定时删除任务 * */ private static void copy(Path filePath, HttpServletResponse response, ContentRange contentRange) { String fileName = filePath.getFileName().toString(); // 要下载的长度 long contentLength = applyAsContentLength(contentRange); BufferedOutputStream outputStream = null; RandomAccessFile randomAccessFile = null; // 已传送数据大小 long transmitted = 0; try { randomAccessFile = new RandomAccessFile(filePath.toFile(), "r"); randomAccessFile.seek(contentRange.start); outputStream = new BufferedOutputStream(response.getOutputStream(), BUFFER_SIZE); // 把数据读取到缓冲区中 byte[] buffer = new byte[BUFFER_SIZE]; int len = BUFFER_SIZE; //warning:判断是否到了最后不足4096(buffer的length)个byte这个逻辑((transmitted + len)


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3